Skip to content

[Feature] Native Slack task cards for Fast delegations - #1431

Draft
mrubens wants to merge 11 commits into
developfrom
codex/slack-task-cards-wip
Draft

[Feature] Native Slack task cards for Fast delegations#1431
mrubens wants to merge 11 commits into
developfrom
codex/slack-task-cards-wip

Conversation

@mrubens

@mrubens mrubens commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Fast-delegated tasks now stream a native Slack agent task card into the parent thread, showing live progress alongside the kickoff.

What this does

  • The Fast launcher opens a native task stream (chat.startStream with task_update chunks) in beforeEnqueue: the entry title is the task title, with a View task source link and an immediate repaint append (the Slack client does not render a stream whose only content is its opening chunk).
  • The worker projects callback events onto the card through run-scoped SDK endpoints (sandboxed workers cannot reach control-plane Redis). Field semantics drove the design: title/status replace on append while details/output/sources accumulate — so the entry title carries the current todo with progress (swapping cleanly per step), narrative streams into the output body as newline-prefixed deltas, and the source link is sent exactly once.
  • Card state is keyed by task id and the liveTaskStream payload flag is inherited across snapshot resumes, so resumed runs keep updating the same card; the card-data endpoint overlays the task's generated title.
  • Card callbacks are wired by the payload flag for every payload kind, with an onExit fallback that settles the card (complete, or error with output for cancel/failure) even when the completion event is lost.
  • task_update fields are size-capped and Slack response warnings are logged, so silently dropped chunks are visible.

Open items

  • Body length discipline: suppress transient status lines (provider retries) and limit narrative appends, since accumulated output cannot be edited afterward
  • Investigate the warning icon observed on a long-running card (possible stream lifetime limit or failed append)
  • Emit tool-action events from the runtime envelope so the card can show recent actions
  • Possibly slim the kickoff message now that the card carries the task link

@roomote-community

roomote-community Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

No new code issues found. 1 issue outstanding. See task

  • Preserve todo-row delivery state when Slack rejects an append (apps/worker/src/callbacks/slack-live-task-stream.ts:232)
  • Commit Slack card state only after Slack confirms delivery (apps/worker/src/callbacks/slack-live-task-stream.ts:243-247,257-261)

Reviewed d89a67c

sentStatuses.set(todo.id, rowStatus);
return true;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sentStatuses is updated before appendTaskStream has succeeded. If Slack returns false (or the append throws and the callback catches it), this update permanently records all emitted todo rows as delivered; the next identical todo_update filters them out, so the card never receives its checklist entries. Commit these statuses only after a successful append, or roll them back on failure, and cover the false-return path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by the single-entry card refactor: it no longer records per-todo delivery state before calling Slack.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is reintroduced by the new delta guard: slackLiveTaskLastDetails is set before appendTaskStream has succeeded, while the notifier reports a rejected Slack response as false. A failed append therefore suppresses a later event with the same detail, leaving the card stale. Record the line only after a successful append (or roll it back on failure), and cover the false path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This remains unresolved in the title/output refactor: slackLiveTaskCardTitle and slackLiveTaskLastOutput are recorded before appendTaskStream resolves, and its false result is ignored. A rejected append makes an identical subsequent step or narrative update short-circuit at the delta guard, leaving the card stale. Commit those values only after a successful append and cover the false path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still unresolved. appendTaskStream can return false, but slackLiveTaskCardTitle/slackLiveTaskLastOutput are committed before that call (lines 181-185). The new narration throttle also sets slackLiveTaskStepNarrated before awaiting the append (line 276), so one rejected append suppresses every later narration until a todo update. Commit or restore all delivery guards only after a successful append, and cover the false path.

Fast-delegated tasks now post a live task card into the parent thread
alongside the kickoff, using Slack's native agent task streams
(chat.startStream/appendStream/stopStream with task_update chunks).

- The launcher opens the stream in beforeEnqueue: entry title = task
  title, View task source link (sent once; Slack appends sources per
  chunk), plus an immediate repaint append (the client does not render a
  stream whose only content is the opening chunk)
- The worker projects callback events onto the card through run-scoped SDK
  endpoints (sandboxed workers cannot reach control-plane Redis): the
  entry title carries the current todo with progress (title/status replace
  on append; details/output accumulate), narrative text streams into the
  output body as newline-prefixed deltas, and request_user_input shows a
  waiting state
- Card state is keyed by task id and the liveTaskStream flag is inherited
  across snapshot resumes, so resumed runs keep updating the same card;
  the card-data endpoint overlays the task's generated title
- Card callbacks are wired by the payload flag for every payload kind,
  with an onExit fallback that settles the card (complete or error with
  output) even when the completion event is lost
- task_update fields are size-capped and Slack response warnings logged,
  so dropped chunks are visible
@mrubens
mrubens force-pushed the codex/slack-task-cards-wip branch from 6af7ec7 to 2247394 Compare August 17, 2026 13:22
mrubens added 10 commits August 17, 2026 09:34
Replace the append-only streaming approach with the task_card Block Kit
block in an ordinary message, re-rendered wholesale via chat.update on
every callback event. The card body always shows just the latest state
(current todo with progress, latest narration line), transient states
vanish on the next render instead of accumulating, and completion swaps
in the full output. Removes the now-unused stream methods from the
notifier.
Back on the streaming card (chat.update re-renders reset the client's
expand state; stream appends preserve it):
- task_display_mode 'dense' collapses consecutive tool-call entries
- Transient status lines (provider retries) never enter the permanent body
- Each step contributes at most one narration line, keeping long runs
  readable
The generated title usually lands right after enqueue; the launcher now
re-titles the card immediately (with one short retry) instead of leaving
the prompt-derived title until settle. Bounded to the pre-worker window
so step titles are never overwritten.
The API rejects 'dense' as an invalid enum value (documented but not
accepted), which failed chat.startStream and dropped the card entirely.
Re-home the Fast live task-card launch onto develop's cloud-agents
launcher: generic afterKickoff/afterLaunch/liveTaskStream hooks on
createFastAgentTaskLauncher, and a Slack-specific
createFastAgentSlackLiveTaskLauncher in @roomote/slack used by both the
api message entry and the sdk parent-event path.
The native task card is the progress surface for Fast turns, so the
automatic eyes reaction (and the isExistingConversation gating that only
existed to suppress it) is gone from the Slack Fast path.
- Title is the only task_update field Slack replaces, so the card now
  shows just the latest progress line there (current todo without a
  step count, latest narration, or waiting state) and writes nothing to
  the body until it settles.
- The worker pushes the generated task title at start (and the launcher
  polls longer for it) so the prompt-derived opening title gets replaced.
- A failed turn no longer settles the card as an error and wipes its
  stream data: the workspace is retained for resume, so the next run of
  the task keeps driving the same card and flips it back to in progress.
- When Slack reports the stream is gone (expired server-side or already
  stopped), the card falls back to chat.update on the same message.
Streamed task_update chunks only replace title/status; details, output and
sources accumulate, which is why the body jammed messages together. Post
the card as a task_card block in an ordinary message instead and re-render
the whole block through chat.update on every change, so the card shows the
task title, the current step, and only the latest agent message. This also
removes the stream lifetime problem (no stream to expire) and the
recipient plumbing that only chat.startStream needed.
Slack generates a new block_id on every chat.update when none is given,
which remounts the task_card in the client and collapses it on each
render. Pin it to the task so every update is the same block.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant